﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BFlatCompile.Project
{
	internal class JsonParser
	{
		private string[] tokens;
		private int index;
		private int length;

		public JsonParser(string text)
		{
			this.tokens = JsonTokenizer.Tokenize(text);
			this.index = 0;
			this.length = this.tokens.Length;
		}

		public JsonObject Parse()
		{
			JsonObject output = this.ParseObject();
			if (this.index < this.length)
			{
				throw new Exception("Only one root JSON element is valid.");
			}
			return output;
		}

		private object ParseThing()
		{
			string firstToken = this.Peek();
			if (firstToken == "null") return null;
			if (firstToken == "true") return true;
			if (firstToken == "false") return false;
			if (firstToken == "[") return this.ParseList();
			if (firstToken == "{") return this.ParseObject();
			if (firstToken[0] == '"') return this.ParseString();
			if (firstToken[0] == '\'') return this.ParseString();
			return this.ParseNumber();
		}

		private object ParseNumber()
		{
			string token = this.Pop();
			int sign = 1;
			if (token[0] == '-')
			{
				sign = -1;
				token = token.Substring(1);
			}

			bool isInteger = true;
			for (int i = 0; i < token.Length; ++i)
			{
				if (token[i] < '0' || token[i] > '9')
				{
					isInteger = false;
				}
			}

			if (isInteger)
			{
				int output;
				if (!int.TryParse(token, out output))
				{
					throw new Exception("Invalid integer value.");
				}
				return output * sign;
			}

			double floatValue;
			if (!double.TryParse(token, out floatValue))
			{
				// consider this is the final else of ParseThing. Error must be generic.
				throw new Exception("Unrecognized valid.");
			}

			return floatValue * sign;
		}

		private JsonList ParseList()
		{
			JsonList output = new JsonList();
			this.PopExpected("[");
			bool commaFound = true;
			while (this.Peek() != "]")
			{
				if (!commaFound)
				{
					throw new Exception("Expected: comma");
				}

				object value = this.ParseThing();
				output.Add(value);
				commaFound = this.PopIfPresent(",");
			}
			this.PopExpected("]");
			return output;
		}

		private string ParseString()
		{
			string rawValue = this.Pop();
			if (rawValue.Length < 2) throw new Exception("Invalid string value");
			char delimiter = rawValue[0];
			StringBuilder output = new StringBuilder();
			char c;
			for (int i = 1; i < rawValue.Length - 1; ++i)
			{
				c = rawValue[i];
				if (c == delimiter)
				{
					throw new Exception("This shouldn't happen.");
				}

				if (c == '\\')
				{
					c = rawValue[++i];
					switch (c)
					{
						case 'n': output.Append("\n"); break;
						case 'r': output.Append("\r"); break;
						case 't': output.Append("\t"); break;
						case '\\': output.Append("\\"); break;
						case '"': output.Append("\""); break;
						case '\'': output.Append("'"); break;
						case '0': output.Append("\0"); break;
						default: throw new Exception("Invalid escape sequence.");
					}
				}
				else
				{
					output.Append(c);
				}
			}

			return output.ToString();
		}

		private JsonObject ParseObject()
		{
			JsonObject output = new JsonObject();
			this.PopExpected("{");
			bool commaFound = true;
			while (this.Peek() != "}")
			{
				if (!commaFound)
				{
					throw new Exception("Expected: comma");
				}
				string key = this.Pop();
				// TODO: verify key is a string or alphanumeric.

				if (key[0] == '"' || key[0] == '\'')
				{
					key = key.Substring(1, key.Length - 2);
				}

				if (key.Length == 0)
				{
					throw new Exception("Cannot have empty key.");
				}

				this.PopExpected(":");
				object value = this.ParseThing();

				output.AddValue(key, value);

				commaFound = this.PopIfPresent(",");
			}
			this.PopExpected("}");
			return output;
		}


		private string Pop()
		{
			if (this.index < this.length)
			{
				return this.tokens[this.index++];
			}
			throw new Exception("Unexpected EOF");
		}

		private string PopExpected(string expectedToken)
		{
			string value = this.Pop();
			if (value != expectedToken)
			{
				throw new Exception("Unexpected token: " + value);
			}
			return value;
		}

		private bool PopIfPresent(string expectedValue)
		{
			string value = this.Peek();
			if (value == expectedValue)
			{
				++this.index;
				return true;
			}
			return false;
		}

		private string Peek()
		{
			if (this.index < this.length)
			{
				return this.tokens[this.index];
			}
			return null;
		}
	}
}
